import { HttpException, HttpStatus } from "@nestjs/common"
import * as path from "path"
import * as fs from "fs-extra"
import { IWonewSer } from "@/typing"
import { defer, Observable, map, concatMap, from, of, tap, catchError } from "rxjs"
import * as shelljs from "shelljs"
import * as sharp from "sharp"
import IHelper from "./helper.service"
import IConstants from "@/core/constants"
import * as puppeteer from "puppeteer"
import * as moment from "moment"
import ILogger from "./logger.service"
import IGlobal from "./global.service"

class FfmpegService {
  build(_task: Partial<IWonewSer.FFmpegBuildTask>) {
    const task: IWonewSer.FFmpegBuildTask = {
      ..._task
    }
    let runningId: string = IHelper.hash(32)
    let startTime = 0
    return of(null).pipe(
      // 剪切
      tap(() => {
        startTime = Date.now()
        ILogger.info(`开始剪切`)
      }),
      concatMap(() => {
        if (!task.config.cut_props_enable) {
          let cutVideo_temp = this.getTempPath("mp4")
          fs.copyFileSync(task.video_path, cutVideo_temp)
          task.cutVideo_temp = cutVideo_temp
          return of(task)
        }
        return this.getVideoDimension(task.video_path).pipe(
          concatMap(dimension => {
            const start: number = task.config.cut_props_start || 0
            const duration = dimension.duration - start - (task.config.cut_props_end || 0)
            return this.cutVideo(task.video_path, start, duration)
          }),
          map(cutVideo_temp => {
            task.cutVideo_temp = cutVideo_temp
            return task
          })
        )
      }),
      tap(() => {
        // @ts-ignore
        ILogger.info(`完成剪切----${moment.duration(Date.now() - startTime, "milliseconds").format("HH:mm:ss.SSS", {trim: false})}`)
      }),
      // 设置去水印
      tap(() => {
        startTime = Date.now()
        ILogger.info(`开始去水印`)
      }),
      concatMap(() => {
        this.getTempPath("mp4")
        if (!task.config.blur_props_enable) {
          let removeWatermark_temp = this.getTempPath("mp4")
          fs.copyFileSync(task.video_path, removeWatermark_temp)
          task.removeWatermark_temp = removeWatermark_temp
          return of(task)
        }
        return this.blurVideo(task.cutVideo_temp, this.buildDelogo(task.config)).pipe(
          map(removeWatermark_temp => {
            task.removeWatermark_temp = removeWatermark_temp
            return task
          })
        )
      }),
      tap(() => {
        // @ts-ignore
        ILogger.info(`完成去水印----${moment.duration(Date.now() - startTime, "milliseconds").format("HH:mm:ss.SSS", {trim: false})}`)
      }),
      // 设置分辨率
      tap(() => {
        startTime = Date.now()
        ILogger.info(`开始设置分辨率`)
      }),
      concatMap(() => this.updateVideoSize(task)),
      tap(() => {
        // @ts-ignore
        ILogger.info(`完成设置分辨率----${moment.duration(Date.now() - startTime, "milliseconds").format("HH:mm:ss.SSS", {trim: false})}`)
      }),
      // 加结尾
      tap(() => {
        startTime = Date.now()
        ILogger.info(`开始加结尾`)
      }),
      concatMap(() => {
        if (!task.config.end_video_props_enable || !task.config.end_video_props_path) {
          const result_temp = this.getTempPath("mp4")
          fs.copyFileSync(task.updateVideoSize_temp, result_temp)
          task.result_temp = result_temp
          return of(task)
        }
        return this.prepareEndVideo(task).pipe(
          concatMap(formattedEndVideo_temp => {
            return this.concatEndVideo(task.updateVideoSize_temp, formattedEndVideo_temp).pipe(
              map(result_temp => {
                task.result_temp = result_temp
                return task
              })
            )
          })
        )
      }),
      tap(() => {
        // @ts-ignore
        ILogger.info(`完成加结尾----${moment.duration(Date.now() - startTime, "milliseconds").format("HH:mm:ss.SSS", {trim: false})}`)
      }),
      tap(() => {
        startTime = Date.now()
        ILogger.info(`开始获取封面`)
      }),
      concatMap(() => 
        this.getVideoCover(task.result_temp).pipe(
          map(cover_temp => {
            task.cover_temp = cover_temp
            return task
          })
        )
      ),
      tap(() => {
        // @ts-ignore
        ILogger.info(`完成获取封面----${moment.duration(Date.now() - startTime, "milliseconds").format("HH:mm:ss.SSS", {trim: false})}`)
      }),
      // 删除临时文件
      map(() => {
        if (task.updateVideoSize_temp) {
          fs.rmSync(task.updateVideoSize_temp)
        }
        if (task.removeWatermark_temp) {
          fs.rmSync(task.removeWatermark_temp)
        }
        if (task.cutVideo_temp) {
          fs.rmSync(task.cutVideo_temp)
        }
        if (task.bg_temp) {
          fs.rmSync(task.bg_temp)
        }
        if (task.formattedEndVideo_temp) {
          fs.rmSync(task.formattedEndVideo_temp)
        }
        return task
      })
    )
  }

  getTempPath(ext: string): string {
    fs.ensureDirSync(path.join(IConstants.publicDir, `uploads/temp`))
    return path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(32)}.${ext}`)
  }

  buildDelogo(config: IWonewSer.FFmpegConfig) {
    return `x=${config.blur_props_x}:y=${config.blur_props_y}:w=${config.blur_props_width}:h=${config.blur_props_height}`
  }

  prepareEndVideo(task: IWonewSer.FFmpegBuildTask): Observable<string> {
    const formattedEndVideo_temp = path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(16)}.mp4`)
    if (task.config.orientation === 1) {
      fs.copyFileSync(task.config.end_video_props_path, formattedEndVideo_temp)
      task.formattedEndVideo_temp = formattedEndVideo_temp
      return of(formattedEndVideo_temp)
    }
    const targetDimension: IWonewSer.VideoDimension = {width: 720, height: 1280, duration: 0}
    return this.setVideoBackground(task.config.end_video_props_path, targetDimension, task.bg_temp).pipe(
      map(formattedEndVideo_temp => {
        task.formattedEndVideo_temp = formattedEndVideo_temp
        return formattedEndVideo_temp
      })
    )
  } 
  // 重置分辨率
  updateVideoSize(task: IWonewSer.FFmpegBuildTask): Observable<IWonewSer.FFmpegBuildTask> {
    const targetDimension: IWonewSer.VideoDimension = task.config.orientation === 1 ? {
      width: 1280, height: 720, duration: 0
    } : { width: 720, height: 1280, duration: 0}
    return defer(() => 
      task.config.orientation === 2 ? this.getPortraitBackgroundFile(task.video_path, task.video_title) : this.getBackgroundFile(targetDimension, "#000000")
    ).pipe(
      concatMap(backgroundFilepath => {
        task.bg_temp = backgroundFilepath
        return this.setVideoBackground(task.removeWatermark_temp, targetDimension, backgroundFilepath).pipe(
          map(updateVideoSize_temp => {
            task.updateVideoSize_temp = updateVideoSize_temp
            return task
          })
        )
      })
    )
  }
  
  downloadBiliVideo(bid: string) {

  }

  // 获取封面
  getVideoCover(sourceVideo: string): Observable<string> {
    const outputCover = path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(32)}.png`)
    return new Observable(observer => {
      const cmd = `ffmpeg -i ${sourceVideo} -ss 00:00:01 -vframes 1 ${outputCover}`
      shelljs.exec(cmd, {silent: true}, function(code: number, stdout: string) {
        const error = new HttpException(`getVideoCover failed.`, HttpStatus.BAD_REQUEST)
        if (code !== 0) {
          ILogger.error(`getVideoCover failed.`)
          ILogger.error(stdout)
          observer.error(error)
          return
        }
        observer.next(outputCover)
        observer.complete()
      })
    })
  }
  getVideoDimension(filepath: string): Observable<IWonewSer.VideoDimension> {
    return new Observable<IWonewSer.VideoDimension>(observer => {
      const cmd: string = `ffprobe -v error -of flat=s=_ -select_streams v:0 -show_entries stream=height,width,duration -of json -i ${filepath}`
      const result = shelljs.exec(cmd, {silent: true})
      const errorException = new HttpException("Get video dimension failed.", HttpStatus.BAD_REQUEST)
      if (result.code !== 0) {
        observer.error(errorException)
        return 
      }
      try {
        const dimension: IWonewSer.VideoDimension = JSON.parse(result.stdout).streams[0]
        observer.next({...dimension, duration: +dimension.duration})
        observer.complete()
      } catch(e) {
        observer.error(errorException)
      }
    })
  }
  
  getPortraitBackgroundFile(video_path: string, video_title: string): Observable<string> {
    return this.getVideoDimension(video_path).pipe(
      concatMap(dimension => this.getBackBackgroundFileFromPuppeteer(dimension, video_title))
    )
  }

  getBackBackgroundFileFromPuppeteer(dimension: IWonewSer.VideoDimension, title: string) {
    const videoSize = `${dimension.width}_${dimension.height}`
    const filepath = path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(16)}.png`)
    const task: {
      browser?: puppeteer.Browser
      page?: puppeteer.Page
    } = {}
    return defer(() => 
      puppeteer.launch({
        headless: true,
        // headless: false,
        protocolTimeout: 360000,
        args: ['--no-sandbox','--disable-setuid-sandbox'],
      }),
    ).pipe(
      tap((browser) => (task.browser = browser)),
      concatMap(() => task.browser.newPage()),
      tap((page) => (task.page = page)),
      concatMap(() => task.page.goto(`${IGlobal.appUrl}/shared/puppeteer-screenshot?title=${title}&video_size=${videoSize}`)),
      concatMap(() => task.page.$("#rootEle")),
      concatMap(ele => ele.screenshot({path: filepath})),
      concatMap(() => task.browser.close()),
      map(() => filepath),
    )
  }

  getBackgroundFile(dimension: IWonewSer.VideoDimension, background?: string): Observable<string> {
    const createOptions: sharp.Create = {
      width: dimension.width,
      height: dimension.height,
      channels: 4,
      background: background || "#ffffff",
    }
    const bgFilename = `uploads/temp/bg-${IHelper.hash(16)}.png`
    const bgFilepath = path.join(IConstants.publicDir, bgFilename)
    fs.ensureFileSync(bgFilepath)
    return from(
      sharp({create: createOptions}).toFile(bgFilepath)
    ).pipe(
      map(() => bgFilepath)
    )
  }

  setVideoBackground(sourceVideoPath: string, backgroundDimension: IWonewSer.VideoDimension, backgroundFilepath: string): Observable<string> {
    const outputVideo = path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(16)}.mp4`)
    return this.getVideoDimension(sourceVideoPath).pipe(
      map(sourceVideoDimension => {
        const bgAspect = backgroundDimension.width / backgroundDimension.height
        const videoAspect = sourceVideoDimension.width / sourceVideoDimension.height
        let vw = backgroundDimension.width, vh = backgroundDimension.height
        if (videoAspect > bgAspect) {
          vh = vw / videoAspect
        } else {
          vw = vh * videoAspect
        }
        let cmd = `ffmpeg -i ${sourceVideoPath} -i ${backgroundFilepath} -filter_complex '[0:v]scale=${vw}:${vh}[fg]; [1:v]scale=${backgroundDimension.width}:${backgroundDimension.height}[bg]; [bg][fg]overlay=(W-w)/2:(H-h)/2:format=auto[out]' -map '[out]' -map 0:a -c:a copy ${outputVideo}`
        return cmd
      }),
      concatMap(cmd => 
        new Observable<string>(observer => {
          shelljs.exec(cmd, {silent: true}, function(code: number, stdout: string, stderr: string) {
            const error = new HttpException(`Transform end video failed.`, HttpStatus.BAD_REQUEST)
            if (code !== 0) {
              ILogger.error(`Transform end video failed`)
              ILogger.error(stdout)
              observer.error(error)
              return
            }
            observer.next(outputVideo)
            observer.complete()
          })
        })
      )
    )
  }

  concatEndVideo(sourceVideo: string, endVideo: string): Observable<string> {
    console.log(`concatEndVideo`, sourceVideo, endVideo)
    const outputVideo = path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(16)}.mp4`)
    // const endVideo = "/var/www/third-vpuber-ns/server/public/assets/video-suffix/01.mp4"
    // const sourceVideo = "/var/www/third-vpuber-ns/server/public/uploads/temp/y4BVO1bZ9uIwI0aE.mp4"
    // const outputVideo = "/var/www/third-vpuber-ns/server/public/uploads/temp/y4BVO1bZ9uIwI0aE-123.mp4"
    // const cmd = `ffmpeg -i ${sourceVideo} -i ${endVideo} -filter_complex '[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]' -map '[outv]' -map '[outa]' ${outputVideo}`
    const cmd = `ffmpeg -i ${sourceVideo} -i ${endVideo} -filter_complex '[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]' -map '[outv]' -map '[outa]' ${outputVideo}`
    // ILogger.info(`concatEndVideo cmd-----${cmd}`)
    return new Observable(observer => {
      shelljs.exec(cmd, {silent: true}, function(code: number, stdout: string) {
        const error = new HttpException(`Concat end video failed.`, HttpStatus.BAD_REQUEST)
        if (code !== 0) {
          ILogger.error(`Concat end video failed.`)
          ILogger.error(stdout)
          observer.error(error)
          return
        }
        observer.next(outputVideo)
        observer.complete()
      })
    })
  }

  cutVideo(sourceVideo: string, start: number, duration: number): Observable<string> {
    // @ts-ignore
    const startStr = moment.duration(start, "seconds").format("HH:mm:ss.SSS", {trim: false})
    // @ts-ignore
    const durationStr = moment.duration(duration, "seconds").format("HH:mm:ss.SSS", {trim: false})
    const outputVideo = path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(16)}.mp4`)
    const cmd = `ffmpeg -ss ${startStr} -i ${sourceVideo} -t ${durationStr} -c:v copy -c:a copy ${outputVideo}`
    console.log("cut cmd", cmd)
    return new Observable(obs => {
      shelljs.exec(cmd, {silent: true}, function(code: number, stdout: string) {
        const error = new HttpException(`Cut video failed.`, HttpStatus.BAD_REQUEST)
        if (code !== 0) {
          ILogger.error(`Cut video failed.`)
          ILogger.error(stdout)
          obs.error(error)
          return
        }
        obs.next(outputVideo)
        obs.complete()
      })
    })
  }

  getTempVideo() {
    return path.join(IConstants.publicDir, `uploads/temp/${IHelper.hash(16)}.mp4`)
  }
  // 去水印
  blurVideo(sourceVideo: string, delogo: string): Observable<string> {
    // delogo=x=20:y=32:w=380:h=70
    // const cmd = `ffplay -f lavfi -i "movie=/Users/eros/Documents/code/wonew/third-vpuber-ns/server/public/uploads/temp/t1DYZhYY6FM4zqCW.mp4,delogo=x=20:y=32:w=380:h=70:show=1"`
    const outputVideo = this.getTempVideo()
    const cmd = `ffmpeg -i ${sourceVideo} -vf 'delogo=${delogo}' -c:a copy ${outputVideo}`
    return new Observable(obs => {
      shelljs.exec(cmd, {silent: true}, function(code: number, stdout: string) {
        const error = new HttpException(`blurVideo failed.`, HttpStatus.BAD_REQUEST)
        if (code !== 0) {
          ILogger.error(`blurVideo failed.`)
          ILogger.error(stdout)
          obs.error(error)
          return
        }
        obs.next(outputVideo)
        obs.complete()
      })
    })
  }

  mergeEndVideo(sourceVideo: string, endVideo: string, background?: string) {
    return this.getVideoDimension(sourceVideo).pipe(
      concatMap(dimension => 
        this.getBackgroundFile(dimension, background).pipe(
          map(backgroundFilepath => ({dimension, backgroundFilepath}))
        )
      ),
      concatMap(res => this.setVideoBackground(endVideo, res.dimension, res.backgroundFilepath)),
      concatMap(scaledEndVideo => this.concatEndVideo(sourceVideo, scaledEndVideo))
    )
  }
}

const IFfmpeg = new FfmpegService()

export default IFfmpeg